package edu.cmsc611.textsimulator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
 * Least Recently used cache implementation
 *
 *
 *
 */
public class LRUCache {

	static LRUCache instance=null;
	LinkedHashMap<Integer, DCacheDataFormat> cache = null;
	private final int cacheSize;
	private int lrukey;
	private int elapseCycle=0;
	private int accessedmemoryby_instructionid;
	private boolean memorybusy=false;
	private int reqCycleforMiss=0;
	private int reqCyclesforHit=1;
	private boolean secondblock=false;
	private boolean firstblock=false;
	private int bothBlockhit=2;
	private int reqTimeToFinsish=0;
	private int insCacheMissOccures=0;
	private int insCacheHitOccures=0;
	private int dataMissOccures=0;
	private int dataHitOccures=0;
	private int insRequested=0;
	private int dataRequested=0;
	


	public int getInsCacheMissOccures() {
		return insCacheMissOccures;
	}
	public void setInsCacheMissOccures(int insCacheMissOccures) {
		this.insCacheMissOccures = insCacheMissOccures;
	}
	public int getInsCacheHitOccures() {
		return insCacheHitOccures;
	}
	public void setInsCacheHitOccures(int insCacheHitOccures) {
		this.insCacheHitOccures = insCacheHitOccures;
	}
	public int getDataMissOccures() {
		return dataMissOccures;
	}
	public void setDataMissOccures(int dataMissOccures) {
		this.dataMissOccures = dataMissOccures;
	}
	public int getDataHitOccures() {
		return dataHitOccures;
	}
	public void setDataHitOccures(int dataHitOccures) {
		this.dataHitOccures = dataHitOccures;
	}
	public int getInsRequested() {
		return insRequested;
	}
	public void setInsRequested(int insRequested) {
		this.insRequested = insRequested;
	}
	public int getDataRequested() {
		return dataRequested;
	}
	public void setDataRequested(int dataRequested) {
		this.dataRequested = dataRequested;
	}
	public int getReqTimeToFinsish() {
		return reqTimeToFinsish;
	}
	public void setReqTimeToFinsish(int reqTimeToFinsish) {
		this.reqTimeToFinsish = reqTimeToFinsish;
	}
	public boolean isSecondblock() {
		return secondblock;
	}
	public int getBothBlockhit() {
		return bothBlockhit;
	}
	public void setBothBlockhit(int bothBlockhit) {
		this.bothBlockhit = bothBlockhit;
	}
	public void setSecondblock(boolean secondblock) {
		this.secondblock = secondblock;
	}
	public boolean isFirstblock() {
		return firstblock;
	}
	public void setFirstblock(boolean firstblock) {
		this.firstblock = firstblock;
	}
	public int getReqCycleforMiss() {
		return reqCycleforMiss;
	}
	public void printlnCache(){


		//then you just access the reversedMap however you like...
		for (Map.Entry entry : cache.entrySet()) {
			System.out.println(entry.getKey() + ", " + entry.getValue());
		}
	}
	public void setReqCycleforMiss(int reqCycleforMiss) {
		this.reqCycleforMiss = reqCycleforMiss;
	}
	public int getReqCyclesforHit() {
		return reqCyclesforHit;
	}
	public void setReqCyclesforHit(int reqCyclesforHit) {
		this.reqCyclesforHit = reqCyclesforHit;
	}
	public boolean isMemorybusy() {
		return memorybusy;
	}
	public void setMemorybusy(boolean memorybusy) {
		this.memorybusy = memorybusy;
	}
	public int getElapseCycle() {
		return elapseCycle;
	}
	public void setElapseCycle(int elapseCycle) {
		this.elapseCycle = elapseCycle;
	}
	public int getAccessedmemoryby_instructionid() {
		return accessedmemoryby_instructionid;
	}
	public void setAccessedmemoryby_instructionid(int accessedmemoryby_instructionid) {
		this.accessedmemoryby_instructionid = accessedmemoryby_instructionid;
	}
	public boolean isDoubleMemoryAccessfinished()
	{
		if(this.reqTimeToFinsish<=this.elapseCycle)
		{
			this.memorybusy=false;
			this.elapseCycle=0;
			return true;
		}
		else return false;
	}
	public boolean isMemoryAccessFinished()
	{
		if((this.reqCycleforMiss+this.reqCyclesforHit)<=this.elapseCycle)
		{
			this.memorybusy=false;
			this.elapseCycle=0;
			return true;
		}
		else return false;
	}
	private DCacheDataFormat lruval;
	private int[][] memblock=new int[4][4];

	private int eldest;
	public LRUCache(int capacity) {
		this.cacheSize = capacity;
		for(int i=0;i<memblock.length;i++)
			for(int j=0;j<memblock[0].length;j++)
				memblock[i][j]=-1;

		//set required cycle
		FunctionalUnitSingleton funit=FunctionalUnitSingleton.getInstance();
		reqCycleforMiss=2*(funit.mainMemAccesstime+funit.dCacheAccesstime);
		//set LRU condition
		cache = new LinkedHashMap<Integer, DCacheDataFormat>(cacheSize, 0.7f, true) {
			@Override
			public boolean removeEldestEntry(Map.Entry<Integer, DCacheDataFormat> entry) {
				return this.size() - 1 >= cacheSize;
			}
		};
	}
	public int[] getMemAddresses(int blockindex)
	{
		return memblock[blockindex];
	}
	public int getBlockNumber(int address)
	{
		for(int i=0;i<memblock.length;i++)
			for(int j=0;j<memblock[0].length;j++)
			{
				if(memblock[i][j]==address)
					return i;

			}
		return -1;
	}

	//if cache hit or miss occures
	public boolean isExistsFirstBlock(int key)
	{
		
		if (cache.get(key) != null)
			return true;
		else return false;
	}

	public boolean isExistsSecondBlock(int key)
	{
		if (cache.get(key) != null)
			return true;
		else return false;
	}
	public boolean loadFromMemToCache(int key)
	{
		DataMemory dm=DataMemory.getInstance();
		int blocknum=dm.getBlockNumber(key);
		int[] address=dm.getMemAddresses(blocknum);
		// check if cache memory is empty
		int cacheblock=0;
		boolean needRemove=false;
		if(cache.size()==0)
		{
			cacheblock=0;
		}
		else if(cache.size()>4 && cache.size()<=8)
		{
			cacheblock=1;
		}
		else if(cache.size()>8 && cache.size()<=12)
		{
			cacheblock=2;
		}
		else if(cache.size()>12 && cache.size()<=16)
		{
			cacheblock=3;
		}
		else if(cache.size()>16)
		{
			cacheblock=lruval.getBlocknum();
			needRemove=true;
		}

		// if the value exists to current position, then need to remove
		if(needRemove)
		{
			for(int addressindex=0;addressindex<memblock[cacheblock].length;addressindex++)
			{
				// if the data updated in cache and not written then write to memory
				if(!cache.get(memblock[cacheblock][addressindex]).isWritten())
					dm.writetoDataMemory(memblock[cacheblock][addressindex], cache.get(memblock[cacheblock][addressindex]).getData());
				//after confirm the update to memory, delete from cache
				cache.remove(memblock[cacheblock][addressindex]);
			}
		}
		DCacheDataFormat cachedata;
		int index=0;
		for(int addressindex=0;addressindex<address.length;addressindex++)
		{
			cachedata=new DCacheDataFormat(address[addressindex],dm.readDataMemory(address[addressindex]),false,cacheblock);
			cache.put(address[addressindex], cachedata);
			memblock[cacheblock][index]=address[addressindex];
			index++;
		}


		this.memorybusy=true;
		this.elapseCycle=1;
		//load finished
		return true;
	}

	public int valForAddress(int key)
	{
		if (cache.get(key) != null)
			return cache.get(key).getData();
		else return -1;
	}
	public void updateCacheValue(int key,int data)
	{
		cache.get(key).setData(data);
		cache.get(key).setWritten(false);
	}
	public int get(int key) {
		//System.out.println("Searching for memory=="+key);
		if (cache.get(key) != null)
			return cache.get(key).getData();
		else
		{
			//miss occures
			DataMemory dm=DataMemory.getInstance();
			int blocknum=dm.getBlockNumber(key);
			int[] address=dm.getMemAddresses(blocknum);
			// check if cache memory is empty
			int cacheblock=0;
			boolean needRemove=false;
			if(cache.size()==0)
			{
				cacheblock=0;
			}
			else if(cache.size()>4 && cache.size()<=8)
			{
				cacheblock=1;
			}
			else if(cache.size()>8 && cache.size()<=12)
			{
				cacheblock=2;
			}
			else if(cache.size()>12 && cache.size()<=16)
			{
				cacheblock=3;
			}
			else if(cache.size()>16)
			{
				cacheblock=lruval.getBlocknum();
				needRemove=true;
			}

			// if the value exists to current position, then need to remove
			if(needRemove)
			{
				for(int addressindex=0;addressindex<memblock[cacheblock].length;addressindex++)
				{
					// if the data updated in cache and not written then write to memory
					if(!cache.get(memblock[cacheblock][addressindex]).isWritten())
						dm.writetoDataMemory(memblock[cacheblock][addressindex], cache.get(memblock[cacheblock][addressindex]).getData());
					//after confirm the update to memory, delete from cache
					cache.remove(memblock[cacheblock][addressindex]);
				}
			}
			DCacheDataFormat cachedata;
			int index=0;
			for(int addressindex=0;addressindex<address.length;addressindex++)
			{
				cachedata=new DCacheDataFormat(address[addressindex],dm.readDataMemory(address[addressindex]),false,cacheblock);
				cache.put(address[addressindex], cachedata);
				memblock[cacheblock][index]=address[addressindex];
				index++;
			}

		}
		this.memorybusy=true;
		this.elapseCycle=1;
		return -1;
	}

	public void set(int key, DCacheDataFormat value) {

		Set<Integer> keysbefore=new HashSet<Integer>(cache.keySet());
		HashMap<Integer,DCacheDataFormat> previousentry=new HashMap<Integer,DCacheDataFormat>(cache);
		cache.put(key, value);
		Set<Integer> keysafter=new HashSet<Integer>(cache.keySet());
		Set<Integer> common=keysbefore;
		common.removeAll(keysafter);
		if(common.size()>0){
			lrukey=common.iterator().next();
			lruval=previousentry.get(lrukey);
		}
	}
	/**
	 * @return the lruval
	 */
	public DCacheDataFormat getLruval() {
		return lruval;
	}

	/**
	 * @param lruval the lruval to set
	 */
	public void setLruval(DCacheDataFormat lruval) {
		this.lruval = lruval;
	}

	/**
	 * @return the lrukey
	 */
	public int getLrukey() {
		return lrukey;
	}

	/**
	 * @param lrukey the lrukey to set
	 */
	public void setLrukey(int lrukey) {
		this.lrukey = lrukey;
	}

	public int getUsedEntries()
	{
		return cache.size();
	}
	public DCacheDataFormat getLeaseRecentObject()
	{
		if(cache.size()==this.cacheSize)
			return getFirstKey();
		return null;
	}
	public DCacheDataFormat getFirstKey() {
		DCacheDataFormat out = null;
		for (int key : cache.keySet()) {
			out = cache.get(key);
			break;
		}
		return out;
	}

	public DCacheDataFormat getLastKey() {
		DCacheDataFormat out = null;
		//for (int key : cache.keySet()) {
		out = cache.get(cache.keySet().size());
		//}
		return out;
	}
	public static LRUCache getInstance(){
		if(instance==null){
			instance=new LRUCache(4);
		}
		return instance;
	}
	public static void main(String[] args) {
		//LRUCache cache = new LRUCache(3);
		ReadInput ri=new ReadInput();
		ri.readData("data.txt");
		LRUCache cachemem=LRUCache.getInstance();
		cachemem.get(256);
		//		DCacheDataFormat obj;
		//
		//		obj=new DCacheDataFormat(256,256,false,0);
		//		cachemem.set(256, obj);
		//		obj=new DCacheDataFormat(260,260,false,0);
		//		cachemem.set(260, obj);
		//		obj=new DCacheDataFormat(264,264,false,1);
		//		cachemem.set(264, obj);
		//		obj=new DCacheDataFormat(268,268,false,1);
		//		cachemem.set(268, obj);
		//
		//
		//		//System.out.println(cachemem.getLastKey());
		//		System.out.println(cachemem.get(256).getData());
		//		System.out.println(cachemem.get(268).getData());
		//		//System.out.println(cachemem.getLastKey().getData());
		//		obj=new DCacheDataFormat(272,272,false,1);
		//		cachemem.set(272, obj);
		//		System.out.println(cachemem.get(272).getData());
		//
		//		System.out.println(cachemem.get(260));
		//cache.set(5, 272);

	}



}
